import protooClient from 'protoo-client'
import * as mediasoupClient from 'mediasoup-client'
import store from './store'
const PC_PROPRIETARY_CONSTRAINTS = {
    optional : [ { googDscp: true } ]
};
const VIDEO_CONSTRAINS = {
    qvga : { width: { ideal: 320 }, height: { ideal: 240 } },
    vga  : { width: { ideal: 640 }, height: { ideal: 480 } },
    hd   : { width: { ideal: 1280 }, height: { ideal: 720  } }
};
const WEBCAM_KSVC_ENCODINGS = [
    { scalabilityMode: 'S3T3_KEY' }
];
const WEBCAM_SIMULCAST_ENCODINGS = [
    { scaleResolutionDownBy: 4, maxBitrate: 500000 },
    { scaleResolutionDownBy: 2, maxBitrate: 1000000 },
    { scaleResolutionDownBy: 1, maxBitrate: 5000000 }
];
export default class RoomClient {
  constructor(options) {
    this._roomId = options.roomId
    this._peerId = options.peerId
    this._protooUrl = options.protooUrl
    this._protoo = null
    this._mediasoupDevice = null
    this._forceH264 = options.forceH264;
    this._forceVP9 = options.forceVP9;
    this._useSimulcast = options.useSimulcast
    this._produce = options.produce // 是否需要生产媒体流， 默认需要
    this._consume = options.consume // 是否消费流媒体, 默认需要
    this._forceTcp = options.forceTcp// 是否强制使用TCP传输， WEBRTC默认使用UDP传输
    this._useDataChannel = options.datachannel // 是否需要数据通道
    // 流媒体生产传输
    this._sendTransport = null
    // 流媒体消费 传输
    this._recvTransport = null
    this._displayName = options.displayName
    this._device = options.device
    // 本地音频媒体流生产者
    this._micProducer = null
    // 外部摄像头
    this._externalVideo = null
    // 外部视频媒体流
    this._externalVideoStream = null
    // 视频流生产者
    this._webcamProducer = null
    // 屏幕分享者
    this._shareProducer = null
    // 网络摄像头媒体设备信息的映射
    this._webcams = null
    this._webcam = { device : null, resolution : 'hd' }
    // 聊天数据生成这
    this._chatDataProducer = null
    this._botDataProducer = null
    this._closed = false
    this._consumers = new Map()
    this._dataConsumers = new Map()
    // 通道测试信号
    this._nextDataChannelTestNumber = 0;
    this._handlerName = ''
  }
  close() {
    if (this._closed) {
      return
    }
    this._closed = true
    this._protoo.close()
    if (this._sendTransport) {
      this._sendTransport.close()
    }
    if (this._recvTransport) {
      this._recvTransport.close()
    }
    store.dispatch('setRoomState', 'closed')
  }
  async join() { // 加入房间 长连接创建过程
    const url = `wss://${this._protooUrl}/?roomId=${this._roomId}&peerId=${this._peerId}`
    this._handlerName = mediasoupClient.detectDevice()
    const protooTransport = new protooClient.WebSocketTransport(url)

    this._protoo = new protooClient.Peer(protooTransport)
    await store.dispatch('setConnecting', true)
    this._protoo.on('open', () => this._joinRoom().then(() => {}))

    this._protoo.on('failed', (err) => {
      store.dispatch('connectedFail', 'WebSocket连接失败')
    })

    this._protoo.on('disconnected', () => {
      if (this._sendTransport) {
        this._sendTransport.close()
        this._sendTransport = null
      }
      if (this._recvTransport) {
        this._recvTransport.close()
        this._recvTransport = null
      }
      store.dispatch('setRoomState', 'closed')
    })

    this._protoo.on('close', () => {
      if (this._closed) {
        return
      }
      this.close()
    })

    this._protoo.on('request', async (request, accept, reject) => {
      switch (request.method) {
        case 'newConsumer':
          console.log('newConsumer = ', request)
          this.requestNewConsumer(request, accept, reject).then(() => {})
          break
        case 'newDataConsumer':
          console.log('newDataConsumer =', request)
          this.requestNewDataConsumer(request, accept).then(() => {})
          break
      }
    })

    this._protoo.on('notification', (notification) => {
      const method = notification.method
      switch (method) {
        case 'producerScore':
          const { producerId, score } = notification.data;
          store.dispatch('setProducerScore', {producerId, score})
          break
        case 'newPeer':
          const peer = notification.data;
          store.dispatch('addPeer', {...peer, consumers: [], dataConsumers: []})
          break
        case 'peerClosed':
          {
            const { peerId } = notification.data;
            store.dispatch('removePeer', peerId)
          }
          break
        case 'peerDisplayNameChanged':
          {
            const { peerId, displayName, oldDisplayName } = notification.data;
            store.dispatch('setPeerDisplayName', {peerId, displayName, oldDisplayName})
          }
          break
        case 'downlinkBwe':
          break
        case 'consumerClosed':
          {
            const { consumerId } = notification.data;
            const consumer = this._consumers.get(consumerId);
            if (!consumer) {
              break
            }
            consumer.close()
            this._consumers.delete(consumerId)
            const { peerId } = consumer.appData;
            store.dispatch('removeConsumer', {consumerId, peerId})
          }
          break
        case 'consumerPaused':
          {
            const { consumerId } = notification.data;
            const consumer = this._consumers.get(consumerId);
            if (!consumer) {
              break
            }
            consumer.pause();
            store.dispatch('setConsumerPaused', {consumerId, type: 'remote'})
          }
          break
        case 'consumerResumed':
          {
            const { consumerId } = notification.data;
            const consumer = this._consumers.get(consumerId);
            if (!consumer) {
              break
            }
            consumer.resume();
            store.dispatch('setConsumerResumed', {consumerId, type: 'remote'})
          }
          break
        case 'consumerLayersChanged':
          {
            const { consumerId, spatialLayer, temporalLayer } = notification.data;
            const consumer = this._consumers.get(consumerId);
            if (!consumer) {
              break
            }
            store.dispatch('setConsumerCurrentLayers', {consumerId, spatialLayer, temporalLayer})
          }
          break
        case 'consumerScore':
        {
          const { consumerId, score } = notification.data;
          store.dispatch('setConsumerScore', {consumerId, score})
        }
          break
        case 'dataConsumerClosed':
        {
          const { dataConsumerId } = notification.data;
          const dataConsumer = this._dataConsumers.get(dataConsumerId);
          if (!dataConsumer) {
            break
          }
          dataConsumer.close();
          this._dataConsumers.delete(dataConsumerId);
          const { peerId } = dataConsumer.appData;
          store.dispatch('removeDataConsumer', {peerId, dataConsumerId})
        }
          break
        case 'activeSpeaker':
          const { peerId } = notification.data;
          store.dispatch('setRoomActiveSpeaker', {peerId})
          break
        default:
      }
    })
  }



  async requestNewConsumer(request, accept, reject) {
    if (!this._consume) {
      reject(403, '拒绝接收媒体流')
      return
    }
    const { peerId, producerId, id, kind, rtpParameters, type, appData, producerPaused } = request.data
    try {
      const consumer = await this._recvTransport.consume({
        id,
        producerId,
        kind,
        rtpParameters,
        appData: {
          ...appData,
          peerId
        }
      })

      this._consumers.set(consumer.id, consumer)

      consumer.on('transportclose', () => {
        this._consumers.delete(consumer.id)
      })

      const { spatialLayers, temporalLayers } = mediasoupClient.parseScalabilityMode(consumer.rtpParameters.encodings[0].scalabilityMode)

      const data = {
        id: consumer.id,
        type: type,
        locallyPaused: false,
        remotelyPaused: producerPaused,
        rtpParameters: consumer.rtpParameters,
        spatialLayers: spatialLayers,
        temporalLayers: temporalLayers,
        preferredSpatialLayer: spatialLayers -1 ,
        preferredTemporalLayer: temporalLayers - 1,
        priority: 1,
        codec: consumer.rtpParameters.codecs[0].mimeType.split('/')[1],
        track: consumer.track
      }

      await store.dispatch('addConsumer', {data, peerId})

      accept()

      // 如果是只开启语音，调用此方法
      if (consumer.kind === 'video' && store.state.me.audioOnly) {
        await this._pauseConsumer(consumer)
      }
    }catch (e) {
    }
  }

  async requestNewDataConsumer(request, accept, reject) {
    if (!this._consume) {
      reject(403, '拒绝消费媒体流')
      return
    }
    if (!this._useDataChannel) {
      reject(403, '拒绝数据通道')
      return
    }

    const { peerId, dataProducerId, id, sctpStreamParameters, label, protocol, appData } = request.data

    try {
      const dataConsumer = await this._recvTransport.consumeData({
        id,
        dataProducerId,
        sctpStreamParameters,
        label,
        protocol,
        appData: {
          ...appData,
          peerId
        }
      })

      this._dataConsumers.set(dataConsumer.id, dataConsumer)

      dataConsumer.on('transportclose', () => {
        this._dataConsumers.delete(dataConsumer.id)
      })

      dataConsumer.on('open', () => {

      })

      dataConsumer.on('close', () => {
        this._dataConsumers.delete(dataConsumer.id)
      })

      dataConsumer.on('error', (error) => {
      })

      dataConsumer.on('message', (message) => {
        if (message instanceof ArrayBuffer) {
          const view = new DataView(message);
          const number = view.getUint32();
          if (number === Math.pow(2, 32) - 1) {
            this._nextDataChannelTestNumber = 0;
            return
          }

          if (number > this._nextDataChannelTestNumber) {
          }
          this._nextDataChannelTestNumber = number + 1;
          return
        }
        else if (typeof message !== 'string') {
          return
        }

        switch (dataConsumer.label) {
          case 'chat':

            break
          case 'bot':
            break
        }
      })
      const temp = {
        id: dataConsumer.id,
        sctpStreamParameters: dataConsumer.sctpStreamParameters,
        label: dataConsumer.label,
        protocol: dataConsumer.protocol
      }
      await store.dispatch('addDataConsumer', temp)
      accept()
    }catch (e) {
    }
  }


  async _pauseConsumer(consumer) {
    if (consumer.paused) return
    try {
      await this._protoo.request('pauseConsumer', { consumerId: consumer.id })
      consumer.pause()
      await store.dispatch('setConsumerPaused', {id: consumer.id, type: 'local'})
    }catch (e) {
    }
  }

  async _joinRoom() { // 创建房间或者是加入房间的过程
    try {
      /**
       *  创建连接到mediasoup路由器以发送和/或接收媒体的端点。
       * **/
      this._mediasoupDevice = new mediasoupClient.Device({
        handlerName: this._handlerName
      })
      /**
       *  向mediasoup服务发送请求 ，获取编码，RTP路由等信息
       * **/
      const routerRtpCapabilities = await this._protoo.request('getRouterRtpCapabilities')

      /**
       *  使用mediasoup路由器的RTP功能加载设备。这就是设备如何知道允许的媒体编解码器和其他设置。
       * **/
      await this._mediasoupDevice.load({routerRtpCapabilities})
      /**
       *  检查设备是否可以生成mediasoup启用媒体编码流  视频流
       * **/
      if (!this._mediasoupDevice.canProduce('video')) {
        await store.dispatch('setMeCanVideo', false)
      }

      /**
       *  音频流
       * **/
      if (!this._mediasoupDevice.canProduce('audio')) {
        await store.dispatch('setMeCanAudio', false)
      }

      /**
       *  浏览器的自动播放策略
       * **/
      if (this._produce){
        const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
        const audioTrack = stream.getAudioTracks()[0];
        audioTrack.enabled = false;
        setTimeout(() => audioTrack.stop(), 120000);
      }
      // 创建流媒体的生产者
      if (this._produce) {
        await this.handleCreateProduce()
      }

      if (this._consume) {
        await this.handleCreateConsume()
      }

      const joinParams = {
        displayName: this._displayName,
        device: this._device,
        rtpCapabilities: this._consume ? this._mediasoupDevice.rtpCapabilities : undefined,
        sctpCapabilities: this._useDataChannel && this._consume ? this._mediasoupDevice.sctpCapabilities : undefined
      }
      const {peers} = await this._protoo.request('join', joinParams)

      if (Array.isArray(peers)) { // 进来没人， 则表示自己是房主
        if (peers.length === 0) {
          await store.dispatch('setPeersId', [])
        } else {
          peers.forEach((peer, key) => {
            store.dispatch('addPeer', {...peer, consumers: [], dataConsumers: []}).then(() => {})
          })
          await store.dispatch('setPeersId', peers)
        }
      }

      console.log('_joinRoom peers =', peers)

      // 打开流媒体生产者的麦和摄像头
      if (this._produce) {
        await this.enableMic()
        await this.enableWebcam()
        this._sendTransport.on('connectionstatechange', (connectionState) => {
          if (connectionState === 'connected') {
             this.enableChatDataProducer().then(() => {})
             this.enableBotDataProducer().then(() => {})
          }
        })
      }
      await store.dispatch('setConnected', true)
      await store.dispatch('setConnecting', false)
    }catch (error) {
      this.close()
    }
  }

  async handleCreateProduce() {
    const params = {
      forceTcp: this._forceTcp,
      producing: true,
      consuming: false,
      sctpCapabilities: this._useDataChannel ? this._mediasoupDevice.sctpCapabilities : undefined
    }
    const transportInfo = await this._protoo.request('createWebRtcTransport', params)

    const { id, iceParameters, iceCandidates, dtlsParameters, sctpParameters } = transportInfo

    this._sendTransport = this._mediasoupDevice.createSendTransport({
      id,
      iceParameters,
      iceCandidates,
      dtlsParameters,
      sctpParameters,
      iceServers: [],
      proprietaryConstraints: PC_PROPRIETARY_CONSTRAINTS
    })


    this._sendTransport.on('connect', ({ dtlsParameters }, callback, errback ) => {
      this._protoo.request('connectWebRtcTransport', {
        transportId: this._sendTransport.id,
        dtlsParameters
      }).then(callback).catch(errback)
    })

    this._sendTransport.on('produce', async ( {kind, rtpParameters, appData}, callback, errorback ) => {
      try {
        const requestParam = {
          transportId: this._sendTransport.id,
          kind,
          rtpParameters,
          appData
        }
        const { id } = await this._protoo.request('produce', requestParam)
        callback({id})
      }catch (error) {
        errorback(error)
      }
    })

    this._sendTransport.on('producedata', async ({sctpStreamParameters, label, protocol, appData}, callback, errorback) => {
      try {
        const reqParam = {
          transportId: this._sendTransport.id,
          sctpStreamParameters,
          label,
          protocol,
          appData
        }
        const { id } = this._protoo.request('produceData', reqParam)
        callback({id})
      }catch (error) {
        errorback(error)
      }
    })
  }

  async handleCreateConsume() {
    const params = {
      forceTcp: this._forceTcp,
      producing: false,
      consuming: true,
      sctpCapabilities: this._useDataChannel ? this._mediasoupDevice.sctpCapabilities : undefined
    }
    const transportInfo = await this._protoo.request('createWebRtcTransport', params)


    const { id, iceParameters, iceCandidates, dtlsParameters, sctpParameters } = transportInfo

    this._recvTransport = this._mediasoupDevice.createRecvTransport({
      id,
      iceParameters,
      iceCandidates,
      dtlsParameters,
      sctpParameters,
      iceServers: []
    })


    this._recvTransport.on('connect', ({ dtlsParameters }, callback, errorback) => {
      this._protoo.request('connectWebRtcTransport', {
        transportId: this._recvTransport.id,
        dtlsParameters
      }).then(callback).catch(errorback)
    })
  }

  async enableMic() {
    if (this._micProducer) return
    if (!this._mediasoupDevice.canProduce('audio')) {
      return
    }
    let track
    try {
      if (!this._externalVideo) {
        const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
        track = stream.getAudioTracks()[0];
      } else {
        const stream = await this._getExternalVideoStream();
        track = stream.getAudioTracks()[0].clone();
      }

      this._micProducer = await this._sendTransport.produce({
        track,
        codecOptions: {
          opusStereo: 1,
          opusDtx: 1
        }
      })

      console.log('addProducer 添加音频生产者', this._micProducer)
      await store.dispatch('addProducer', {
        id: this._micProducer.id,
        paused: this._micProducer.paused,
        track: this._micProducer.track,
        rtpParameters: this._micProducer.rtpParameters,
        codec: this._micProducer.rtpParameters.codecs[0].mimeType.split('/')[1]
      })

      this._micProducer.on('transportclose', () => {
        this._micProducer = null
      })

      this._micProducer.on('trackended', () => {
        this.disableMic().then(() => {})
      })
    }catch (error) { // 音频流生产打开失败
      if (track) {
        track.stop()
      }
    }
  }

  async disableMic() {
    if (this._micProducer) {
      return
    }
    this._micProducer.close()
    await store.dispatch('removeProducer', {id: this._micProducer.id})
    try {
      await this._protoo.request('closeProducer', {producerId: this._micProducer.id})
    }catch (e) {
    }
    this._micProducer = null
  }

  async disableShare() {
    if (!this._shareProducer) {
      return
    }
    this._shareProducer.close()
    await store.dispatch('removeProducer', {id: this._shareProducer.id})
    try {
      await this._protoo.request('closeProducer', {producerId: this._shareProducer.id})
    }catch (e) {
    }
    this._shareProducer = null
  }


  // 打开视频，且添加媒体流生产者
  async enableWebcam() {
    if (this._webcamProducer) {
      return
    } else if (this._shareProducer) {
      await this.disableShare()
    }
    if (!this._mediasoupDevice.canProduce('video')) {
      return
    }

    let track, device;
    await store.dispatch('setWebcamInProgress', true)
    try {
      if (this._externalVideo) {
        device = { label: 'external video' };
        const stream = await this._getExternalVideoStream();
        track = stream.getVideoTracks()[0].clone();
      }
      else {
        await this._updateWebcams();
        device = this._webcam.device;
        const { resolution } = this._webcam;
        if (!device) {
          throw new Error('no webcam devices');
        }
        const stream = await navigator.mediaDevices.getUserMedia(
          {
            video :
              {
                deviceId : { ideal: device.deviceId },
                ...VIDEO_CONSTRAINS[resolution]
              }
          });

        track = stream.getVideoTracks()[0];
      }

      let encodings, codec;
      const codecOptions = {
        videoGoogleStartBitrate: 1000
      }

      if (this._forceH264) {
        codec = this._mediasoupDevice.rtpCapabilities.codecs.find((c) => c.mimeType.toLowerCase() === 'video/h264');
        if (!codec) {
          throw new Error('desired H264 codec+configuration is not supported');
        }
      } else if (this._forceVP9) {
        codec = this._mediasoupDevice.rtpCapabilities.codecs.find((c) => c.mimeType.toLowerCase() === 'video/vp9');
        if (!codec) {
          throw new Error('desired VP9 codec+configuration is not supported');
        }
      }

      if (this._useSimulcast) {
        const firstVideoCodec = this._mediasoupDevice.rtpCapabilities.codecs.find((c) => c.kind === 'video');
        if ( (this._forceVP9 && codec) || firstVideoCodec.mimeType.toLowerCase() === 'video/vp9') {
          encodings = WEBCAM_KSVC_ENCODINGS;
        } else {
          encodings = WEBCAM_SIMULCAST_ENCODINGS;
        }
      }

      this._webcamProducer = await this._sendTransport.produce({
        track,
        encodings,
        codecOptions,
        codec
      })

      console.log('addProducer 添加视频生产者', this._webcamProducer)

      await store.dispatch('addProducer', {
        id: this._webcamProducer.id,
        deviceLabel: device.label,
        type: this._getWebcamType(device),
        paused: this._webcamProducer.paused,
        track: this._webcamProducer.track,
        rtpParameters: this._webcamProducer.rtpParameters,
        codec: this._webcamProducer.rtpParameters.codecs[0].mimeType.split('/')[1]
      })

      this._webcamProducer.on('transportclose', () => {
        this._webcamProducer = null
      })

      this._webcamProducer.on('trackended', () => {
        this.disableWebcam().then(() => {})
      })
    }catch (error) {
      if (track) {
        track.stop()
      }
    }

  }

  _getWebcamType(device) {
    if (/(back|rear)/i.test(device.label)) {
      return 'back';
    }
    else {
      return 'front';
    }
  }

  async disableWebcam() {
    if (this._webcamProducer) {
      return
    }
    this._webcamProducer.close()
    await store.dispatch('removeProducer', {id: this._webcamProducer.id})
    try {
      await this._protoo.request('closeProducer', {producerId: this._webcamProducer.id})
    }catch (e) {
    }
    this._webcamProducer = null
  }


  async enableChatDataProducer() {
    if (!this._useDataChannel) {
      return
    }
    try {
      this._chatDataProducer = await this._sendTransport.produceData({
        ordered: false,
        maxRetransmits: 1,
        label: 'chat',
        priority: 'medium',
        appData: {
          info: 'my-chat-DataProducer'
        }
      })

      this._chatDataProducer.on('transportclose', () => {
        this._chatDataProducer = null
      })

      this._chatDataProducer.on('open', () => {
      })

      this._chatDataProducer.on('close', () => {
        this._chatDataProducer = null
      })

      this._chatDataProducer.on('error', (error) => {
      })

      this._chatDataProducer.on('bufferedamountlow', () => {
      })
    }catch (error) {
      throw error
    }
  }

  async enableBotDataProducer() {
    if (!this._useDataChannel) {
      return
    }
    try {
      this._botDataProducer = await this._sendTransport.produceData({
        ordered: false,
        maxPacketLifeTime: 2000,
        label: 'bot',
        priority: 'medium',
        appData: {
          info: 'my-bot-DataProducer'
        }
      })

      this._botDataProducer.on('transportclose', () => {
        this._botDataProducer = null
      })

      this._botDataProducer.on('open', () => {
      })

      this._botDataProducer.on('close', () => {
        this._botDataProducer = null
      })

      this._botDataProducer.on('error', (error) => {
      })

      this._botDataProducer.on('bufferedamountlow', () => {
      })
    }catch (error) {
      throw error
    }
  }
  async _getExternalVideoStream() {
    if (this._externalVideoStream) {
      return this._externalVideoStream
    }
    if (this._externalVideo && this._externalVideo.readyState < 3) {
      await new Promise((resolve, reject) => {
        this._externalVideo.addEventListener('canplay', resolve)
      })
    }

    if (this._externalVideo.captureStream) {
      this._externalVideoStream = this._externalVideo.captureStream();
    } else if (this._externalVideo.mozCaptureStream) {
      this._externalVideoStream = this._externalVideo.mozCaptureStream();
    } else {
      throw new Error('video.captureStream() not supported');
    }

    return this._externalVideoStream
  }
  async _updateWebcams() {
    this._webcams = new Map();
    const devices = await navigator.mediaDevices.enumerateDevices();
    for (let dev in devices) {
      if (devices.hasOwnProperty(dev)) {
        if (devices[dev].kind !== 'videoinput') {
          continue
        }
        this._webcams.set(devices[dev].deviceId, devices[dev]);
      }
    }

    const arr = Array.from(this._webcams.values());
    const len = arr.length;
    const currentWebcamId = this._webcam.device ? this._webcam.device.deviceId : undefined;
    if (len === 0) {
      this._webcam.device = null;
    } else if (!this._webcams.has(currentWebcamId)) {
      this._webcam.device = arr[0];
    }
  }
  async muteMic() {
    this._micProducer.pause()
    try {
      await this._protoo.request('pauseProducer', {producerId: this._micProducer.id})
      await store.dispatch('setProducerPaused', {id: this._micProducer.id})
    }catch (e) {
    }
  }
  async unmuteMic() {
    this._micProducer.resume()
    try {
      await this._protoo.request('resumeProducer', {producerId: this._micProducer.id})
      await store.dispatch('setProducerResumed', {id: this._micProducer.id})
    }catch (e) {
    }
  }
}
